/* 2014/10/24 */
package com.cosmos.log4j;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

import org.apache.log4j.DailyRollingFileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * ZipDailyRollingFileAppender extends <code>DailyRollingFileAppender</code> so that
 * the underlying file is roll over at a user chosen frequency and file will be compressed
 * to zip file.
 * 
 * <p>Because modifier of <code>rollOver</code> method is default, some code is
 * copied from <code>DailyRollingFileAppender</code> to add compress file logic.
 * 
 * @author Leonardo
 * 
 */
public class ZipDailyRollingFileAppender extends DailyRollingFileAppender {

	protected String scheduledFilename;
	protected long nextCheck = System.currentTimeMillis() - 1;
	protected Date now = new Date();
	protected SimpleDateFormat sdf;
	protected RollingCalendar rc = new RollingCalendar();
	// The gmtTimeZone is used only in computeCheckPeriod() method.
	protected static final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");

	protected static final int TOP_OF_TROUBLE = -1;
	protected static final int TOP_OF_MINUTE = 0;
	protected static final int TOP_OF_HOUR = 1;
	protected static final int HALF_DAY = 2;
	protected static final int TOP_OF_DAY = 3;
	protected static final int TOP_OF_WEEK = 4;
	protected static final int TOP_OF_MONTH = 5;

	protected int checkPeriod = TOP_OF_TROUBLE;

	/**
	 * Whether the log file be deleted after it has been compressed to zip file.
	 */
	protected boolean deleteAfterZip = false;

	public ZipDailyRollingFileAppender() {

	}

	public ZipDailyRollingFileAppender(Layout layout, String filename,
			String datePattern) throws IOException {
		super(layout, filename, datePattern);
	}

	public boolean isDeleteAfterZip() {
		return deleteAfterZip;
	}

	public void setDeleteAfterZip(boolean deleteLogAfterZip) {
		this.deleteAfterZip = deleteLogAfterZip;
	}

	@Override
	public void activateOptions() {
		super.activateOptions();

		if (getDatePattern() != null && fileName != null) {
			now.setTime(System.currentTimeMillis());
			sdf = new SimpleDateFormat(getDatePattern());
			int type = computeCheckRollingPeriod();
			rc.setType(type);
			File file = new File(fileName);
			scheduledFilename = fileName
					+ sdf.format(new Date(file.lastModified()));
		}
	}

	protected int computeCheckRollingPeriod() {
		RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone,
				Locale.getDefault());
		// set sate to 1970-01-01 00:00:00 GMT
		Date epoch = new Date(0);
		if (getDatePattern() != null) {
			for (int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
				SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
						getDatePattern());
				simpleDateFormat.setTimeZone(gmtTimeZone); // do all date
															// formatting in GMT
				String r0 = simpleDateFormat.format(epoch);
				rollingCalendar.setType(i);
				Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
				String r1 = simpleDateFormat.format(next);
				// System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
				if (r0 != null && r1 != null && !r0.equals(r1)) {
					return i;
				}
			}
		}
		return TOP_OF_TROUBLE; // Deliberately head for trouble...
	}

	@Override
	protected void subAppend(LoggingEvent event) {
		super.subAppend(event);
		
		long n = System.currentTimeMillis();
		if (n >= nextCheck) {
			now.setTime(n);
			nextCheck = rc.getNextCheckMillis(now);
			
			File scheduledFile = new File(scheduledFilename);
			/*
			 * Compresses log file to zip.
			 */
			if (scheduledFile.exists()) {
				boolean zipSuccess = false;
				SingleFileZipper zipper = new SingleFileZipper();
				try {
					zipper.zip(scheduledFile, scheduledFile.getName() + ".zip");
					zipSuccess = true;
				} catch (IOException e) {
					LogLog.error("Compress log file failed.");
				}

				if (zipSuccess && deleteAfterZip) {
					scheduledFile.delete();
				}
			}

			scheduledFilename = fileName + sdf.format(now);
		}	
	}
}

class RollingCalendar extends GregorianCalendar {

	private static final long serialVersionUID = -4710247370978285691L;

	int type = ZipDailyRollingFileAppender.TOP_OF_TROUBLE;

	RollingCalendar() {
		super();
	}

	RollingCalendar(TimeZone tz, Locale locale) {
		super(tz, locale);
	}

	void setType(int type) {
		this.type = type;
	}

	public long getNextCheckMillis(Date now) {
		return getNextCheckDate(now).getTime();
	}

	public Date getNextCheckDate(Date now) {
		this.setTime(now);

		switch (type) {
		case ZipDailyRollingFileAppender.TOP_OF_MINUTE:
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			this.add(Calendar.MINUTE, 1);
			break;
		case ZipDailyRollingFileAppender.TOP_OF_HOUR:
			this.set(Calendar.MINUTE, 0);
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			this.add(Calendar.HOUR_OF_DAY, 1);
			break;
		case ZipDailyRollingFileAppender.HALF_DAY:
			this.set(Calendar.MINUTE, 0);
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			int hour = get(Calendar.HOUR_OF_DAY);
			if (hour < 12) {
				this.set(Calendar.HOUR_OF_DAY, 12);
			} else {
				this.set(Calendar.HOUR_OF_DAY, 0);
				this.add(Calendar.DAY_OF_MONTH, 1);
			}
			break;
		case ZipDailyRollingFileAppender.TOP_OF_DAY:
			this.set(Calendar.HOUR_OF_DAY, 0);
			this.set(Calendar.MINUTE, 0);
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			this.add(Calendar.DATE, 1);
			break;
		case ZipDailyRollingFileAppender.TOP_OF_WEEK:
			this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
			this.set(Calendar.HOUR_OF_DAY, 0);
			this.set(Calendar.MINUTE, 0);
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			this.add(Calendar.WEEK_OF_YEAR, 1);
			break;
		case ZipDailyRollingFileAppender.TOP_OF_MONTH:
			this.set(Calendar.DATE, 1);
			this.set(Calendar.HOUR_OF_DAY, 0);
			this.set(Calendar.MINUTE, 0);
			this.set(Calendar.SECOND, 0);
			this.set(Calendar.MILLISECOND, 0);
			this.add(Calendar.MONTH, 1);
			break;
		default:
			throw new IllegalStateException("Unknown periodicity type.");
		}
		return getTime();
	}
}
